Identificar por medio de modelos de predicción potenciales cuentas que puedan ser incobrables en un futuro, para ofrecer una liquidación por medio de un arreglo de pago.
Identificar cuales son los atributos que más influyen para determinar si una cuenta se le debe realizar la oferta de liquidación debido a que es catalogada como incobrabl
Obtener al menos un modelo que presente un accuracy (métrica de precisión para exactitud) mayor a un 70% con la utilización de datos externos a los utilizados en el entrenamiento.
Optimizar con diferentes parámetros al menos tres modelos utilizados para la predicción en la categoría de clasificación binaria.
El conjunto de datos es sobre información de campaña de recoleccion de dineros de cuentas en mora, es la información sobre clientes contactados, con una oferta concreta para cerrar su cuenta en mora por un monto inferior al actual.
El conjunto de datos esta separado por dos archivos CSV, los cuales vamos a unir
import pandas as pd
import matplotlib.pyplot as plt
df1 = pd.read_csv('proyectoF1T.csv')
df2 = pd.read_csv('proyectoF2F.csv')
print(df1.shape)
df1.head()
Por lo cual se puede ver la cantidad de registros y columnas.
Cantidad Registros: 6095
Cantidad de columnas: 31
df1 = df1.append(df2)
df2 = None
print(df1.shape)
df1.columns
id int64
approval_amount float64
recommendation string
redeem_ts date
contacted bool
email_contacted_ts date
sms_contacted_ts date
birth_dt date
state string
city string
zip_code int64
rent_or_own string
months_at_current_residence int64
years_at_current_residence int64
direct_deposit bool
black_listed bool
language_preference string
military_applicant bool
payment_frequency string
scheduled_payment_amt float64
past_due_amt float64
account_payment_method string
collections_category string
days_past_due int64
number_of_payments int64
amount_financed float64
data_correction bool
merchant_id int64
source string
initial_decision string
last_payment_amount float64
df1.describe()
Por medio del describe se nos muestra un summary de cuartiles, cantidad de registros,la media y desviación estandar
Para esta sección se aplicaran ambos casos, aunque también se ha aplicado exploración de los datos en la sección anterior.
En la siguiente sección se evaluara la cantidad de nulos, esta cantidad sera mostrada por medio del porcentaje de nulos para cada columna.
df1.isna().mean().round(4)
Se puede observar existe varias columnas que contienen un alto porcentaje de nulos, por lo cual en la siguiente fase seran evaluadas.
Algunas variables presentan valores nulos, que en una representación binaria serian de un valor cero y si contienen datos serian un valor 1.
sms_contacted_ts
Esta columna aplica para los nulos equivalen a personas que no fueron contactadas por SMS, esta información si es importante para nuestro análisis, en la siguiente fase remplazaremos estos valores por valores booleanos para identificar las personas contactadas por este medio.
emal_contacted
Esta columna aplica para los nulos equivalen a personas que no fueron contactadas por email, esta información si es importante para nuestro análisis, en la siguiente fase remplazaremos estos valores por valores booleanos para identificar las personas contactadas por este medio.
redeem_ts
Un caso muy similar a las otra columnas donde los valores nulos serian personas que no se le presentaron oferta, y las instancias con valores de fecha serian las personas que si se le presentaron una oferta.
direct_deposit
Columna Booleana que indica si se hizo un depósito directo, para el caso de los valores nulos que se encuentran seran tomados como falsos.
Durante esta sección veremos algunos datos descartados y limpieza de valores nulos.
Como se menciona anteriormente tratamos las variables con valores nulos que pueden ser interpretados como booleanos.
df1.loc[~df1.email_contacted_ts.isnull(),"email_contacted_ts"]=True
df1.loc[~df1.sms_contacted_ts.isnull(),"sms_contacted_ts"]=True
df1.loc[~df1.direct_deposit.isnull(),"direct_deposit"]=True
df1.loc[~df1.redeem_ts.isnull(),"redeem_ts"]=True
df1["email_contacted_ts"].fillna(False, inplace = True)
df1["sms_contacted_ts"].fillna(False, inplace = True)
df1["direct_deposit"].fillna(False, inplace = True)
df1["redeem_ts"].fillna(False, inplace = True)
df1.head()
Nuevamente se vuelve a comprobar los valores que son nulos.
df1.isna().mean().round(4)
En este proceso se realiza la primera escogencia de datos.
Al observar las siguientes columnas que contienen una gran cantidad de nulos seran borradas:
collections_category
amount_financed
last_payment_amount
Por otra parte se decide borrar un porcentaje pequeño de filas equivalente al 0.0003,
pertenecientes a valores nulos
df1 = df1.drop(columns=['collections_category','amount_financed','last_payment_amount'])
df1.shape
Se borran el 0.0003 de filas pertenecientes a valores nulos de city
df1 = df1.dropna()
df1.shape
df1.isna().mean().round(4)
Por lo tanto en este momento el dataset queda sin valores nulos
Dichas columnas no presentan valor para los algoritmos de clasificación, que contienen un ID unico para cada columna
df1 = df1.drop(columns=['merchant_id','id'])
df1.shape
Por lo tanto se han descartado u total de 5 variables y borrado todo los datos nulos.
Al terminar esta sección de limpieza de datos se identifican variables con alto porcentaje de datos nulos donde es mejor eliminarlas ya que possen mas de un 40% de valores nulos.
Por otra parte existen columnas que son ID para cada fila, estas columnas tambien son borradas ya que no presenta un valor para los algoritmos de machine learning.
Para probar el random forest para detectar las variables mas importantes, tranfomaremos las variables categóricas de forma ordinal, ya que los desicion trees no se ven tan afectados por datos categóricos que son transformados por esta forma.
Los datos categoricos a transformar son:
redeem_ts
contacted
email_contacted_ts
sms_contacted_ts
state
city
payment_frequency
rent_or_own
account_payment_method
data_correction
source
initial_decision
direct_deposit
language_preference
recomendation
military_applicant
black_listed
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import OrdinalEncoder
encoder = OrdinalEncoder()
df1.redeem_ts = encoder.fit_transform(df1.redeem_ts.values.reshape(-1, 1))
df1.contacted = encoder.fit_transform(df1.contacted.values.reshape(-1, 1))
df1.email_contacted_ts = encoder.fit_transform(df1.email_contacted_ts.values.reshape(-1, 1))
df1.sms_contacted_ts = encoder.fit_transform(df1.sms_contacted_ts.values.reshape(-1, 1))
df1.state = encoder.fit_transform(df1.state.values.reshape(-1, 1))
df1.city = encoder.fit_transform(df1.city.values.reshape(-1, 1))
df1.payment_frequency = encoder.fit_transform(df1.payment_frequency.values.reshape(-1, 1))
df1.rent_or_own = encoder.fit_transform(df1.rent_or_own.values.reshape(-1, 1))
df1.account_payment_method = encoder.fit_transform(df1.account_payment_method.values.reshape(-1, 1))
df1.data_correction = encoder.fit_transform(df1.data_correction.values.reshape(-1, 1))
df1.source = encoder.fit_transform(df1.source.values.reshape(-1, 1))
df1.initial_decision = encoder.fit_transform(df1.initial_decision.values.reshape(-1, 1))
df1.direct_deposit = encoder.fit_transform(df1.direct_deposit.values.reshape(-1, 1))
df1.language_preference = encoder.fit_transform(df1.language_preference.values.reshape(-1, 1))
df1.recommendation = encoder.fit_transform(df1.recommendation.values.reshape(-1, 1))
df1.military_applicant = encoder.fit_transform(df1.military_applicant.values.reshape(-1, 1))
df1.black_listed = encoder.fit_transform(df1.black_listed.values.reshape(-1, 1))
df1.head()
Para este caso vamos a construir una nueva variable age, apartir de la variable birth_dt, por lo tanto seria un nuevo atributo.
now = pd.Timestamp('now')
df1['birth_dt'] = pd.to_datetime(df1['birth_dt'])
df1['age'] = (now - df1['birth_dt']).astype('<m8[Y]')
df1 = df1.drop(columns=['birth_dt'])
df1.age.head()
Para la selección final de datos vamos a aplicar un random forest el cual no permite escoger por medio de un nivel de importancia los atributos que mas no favorecen a la hora entrenar los algoritmos de predicción
En nuestro caso la columna redeem_ts viene siendo nuestro label para aplicar la clasificación, por lo tanto se realiza un balance entre cuendo se realizo oferta y no se realizo oferta.
Se convierte la columna redeem_ts en one hot coding
redeem = df1[(df1['redeem_ts'] == 1)]
noRedeem = df1[(df1['redeem_ts'] == 0)]
print(redeem.shape,noRedeem.shape)
Al observar los datos se puede ver que existen más datos de no oferta a oferta, por lo cual se escogen 2595 filas de no oferta para realizar un correcto balance
dfBalanceado = pd.concat([redeem,noRedeem.sample(redeem.shape[0])])
Se separan los features y labels
labels = df1['redeem_ts']
features = df1.drop(columns=['redeem_ts'])
Se separan los datos de entrenamiento para el random forest
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(features,labels,test_size=0.15,random_state=13)
Se presenta una configuración recomenda para dicha tarea, mas adelante en la fase de modelado se aplicara una mejor escogencia de parámetros para este mismo algoritmo.
from sklearn.ensemble import RandomForestClassifier
RandomForestModel = RandomForestClassifier(min_samples_leaf=3,min_samples_split=20,n_estimators=500, max_depth= None, random_state=10)
RandomForestModel.fit(X_train, y_train)
importances = pd.DataFrame({'feature' :features.columns, 'importance':RandomForestModel.feature_importances_})
importances = importances.sort_values('importance', ascending = False).set_index('feature')
importances
Por lo tanto de acuerdo al análisis del random forest se puede observar que los datos mas impotantes que presentan al menos un 1% del total de importancia:
importances.index[:16]
Antes de selecionar estos datos, como hemos realizado la limpieza de datos vamos a realizar un graficación de los datos, cabe destacar que este apartado también pertenece a la sección entendimiento de los datos y exploración de los datos, con la graficación podemos enteder mejor las variables.
La siguiente función es utilizada para graficar los histogramas de cada columna
def plotHistrogram(features):
for i in features:
print("Columna a graficar: ",i)
classes = pd.value_counts(dfBalanceado[i][dfBalanceado.redeem_ts == 1], sort = True)
classes.plot(kind = 'bar', rot=0)
plt.title("Se realizo Oferta")
plt.xlabel(i)
plt.ylabel("Frequency")
plt.show()
classes = pd.value_counts(dfBalanceado[i][dfBalanceado.redeem_ts == 0], sort = True)
classes.plot(kind = 'bar', rot=0)
plt.title("No se realizo oferta")
plt.xlabel(i)
plt.ylabel("Frequency")
plt.show()
Algunos gráficos no se podran enterder muy bien debido a la naturaliza de los datos.
plotHistrogram(importances.index)
Existen columnas o variables del dataset que son difíciles de apreciar por medio de estos gráficos, sin embargo analizaremos los mas destacados.
Se analizara la distribución entre las cuentas que se les ofrecio una oferta y no las que no se les ofrecio, asi como su distribución. Por ejemplo:
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('ggplot')
Funciones para graficar dos variables, permitiendo observar la relación que existen entre ellas, por limitante de los datos algunos gráficos no se podran ver bien.
def plotPoints(data,indexT,indexF,xName,yName):
X = np.array(data[[xName,yName]])
redeem = X[indexT]
noRedeem = X[indexF]
plt.scatter([s[0] for s in redeem], [s[1] for s in redeem], s = 25, color = 'red', edgecolor = 'k')
plt.scatter([s[0] for s in noRedeem], [s[1] for s in noRedeem], s = 25, color = 'cyan', edgecolor = 'k')
plt.xlabel(xName)
plt.ylabel(yName)
plt.show()
plotted = list()
def plotRelationsWithColumns(data,indexT,indexF):
columns = data.columns
for i in columns:
plotted.append(i)
for k in columns:
if (i != k and k not in plotted ):
print("Variables:",k,i)
plotPoints(data,indexT,indexF[:len(indexT)],i,k)
import numpy as np
redeem_true_indexSecond = np.where(dfBalanceado['redeem_ts']==1)[0]
redeem_false_indexSecond = np.where(dfBalanceado['redeem_ts']==0)[0]
plotRelationsWithColumns(features,redeem_true_indexSecond,redeem_false_indexSecond)
Por medio de las siguientes gráficos podemos observar que las siguientes variables presentan mayor cantidad de dispersión.
Por lo tanto de acorde a los datos que interpretamos con el random forest y estos graficos podemos entender que las variables mas importantes son las siguientes:
Por lo cual guardaremos las variables que se han escogido, recordar que en este momento se cumplen varios objectivos de la fase del entendimiento del negocio, donde se presentan varios atributos importantes para evaluar las cuentas de crédito.
newFeatures = features[["account_payment_method","approval_amount","scheduled_payment_amt","days_past_due","past_due_amt" ,"age","zip_code","city","sms_contacted_ts","state","years_at_current_residence","months_at_current_residence","source","email_contacted_ts","payment_frequency","number_of_payments"]]
newFeatures['redeem_ts'] = labels
newFeatures.to_csv("DatosEscogidos.csv")
En caso de datos categóricos con one hot encoding, utilizamos el siguiente algoritmo:
MLPDfp2 = pd.get_dummies(newFeatures)
newFeatures.to_csv("df_normalized.csv")
Para este caso utilizaremos varias jupyternotebooks debido a que cada una consume muchos recursos y al utilizar un computador personal estos son muy limitados por lo cual solo utilizar un jupyternotebook para todo seria una gran carga para un computador personal.
Cada modelo describe las siguientes fases:
Enlace para el jupyternotebook de redes neuronales.
Enlace para el jupyternotebook de arboles de decisión y random forest.
Enlace para el jupyternotebook de catboost.
Se genera una red con las siguientes layer:
1 - 16 neuronas, función de activación: relu, drop-out: 0.3
2 - 8 neuronas, función de activación: tanh, drop-out: 0.2
3 - salida 1 neuronas, función de activación: sigmoid
Los functión loss: categorical_hinge Optimizador: Adamax
Este fue el mejor modelo para las redes neuronales dando los siguientes resultados:
En el caso de desicion tree el mejor accuracy usando la técnica cross-validation es 0.74 con los hyper-parametros
max_depth = 7
min_sample_leaf = 10
Accuracy = 0.74
En el caso del Random Forest el mejor accuracy segun cross-validation es 0.7540 con los hyper-parametros
n_estimator = 1200
max_depth = 17
min_samples_split = 20
min_samples_leaf = 5
Accuracy = 0.7540
En el caso del Catboost el mejor accuracy en general es del primer modelo, presentando el mejor overfitting, entre la diferencia entre el accuracy del training y testing siendo un 50% y 49% respectivamente
Características del primer modelo
El mejor modelo obtenido y por el cual seleccionamos para realizar la predicción de cuentas incobrables es del Random Forest, con dichas características:
Accuracy = 0.7540
Por otra parte con la utilización de este modelo obtuvimos los primeros análisis de importancia de variables y esto nos dio mucha información importante para estudiar cuales variables nos estaban aportando la mayoria de la información y poder eliminar las variables que nos estaban agregando ruido al entrenamiento. Con este modelo logramos obtener los mejores resultados y hasta el momento el modelo escogido para presentar como resultados finales cumpliendo el objetivo de encontrar un modelo que presentara un accuracy mayor a un 70%.